C言語のアロー演算子ってどこまで繋げられるんだろうか

C言語のアロー演算子ってどこまで繋げられるんだろうか

Clock Icon2024.07.04

はじめに

C言語にはアロー演算子と呼ばれる演算子があります。詳細は省きますが、構造体のメンバへのアクセス時に構造体インスタンスが

  • ポインタ型であればアロー(->)
  • そうでなければドット(.)

を使う、くらいの感じです。

この前友人にC言語を教えていたんですが、その際こう質問されました。

「このアロー演算子って2つ繋げられるんですか?」

もちろん繋げられます。

hoge->fuga->piyo;

友人「いくつまで繋げられるんですか?」
僕「知らんよ...」

ということで試してみました。

環境とか

C言語は往々にして"処理系依存"の仕様があります。型のサイズさえ処理系依存なので、他のコンパイラやバージョンやOSでは再現性がない可能性があることを最初におことわりしておきます。

項目 バージョン
OS macOS Sonoma14.5
コンパイラ Apple clang version 15.0.0 (clang-1500.3.9.4)

コード

こんなコードを用意しました。

#include<stdio.h>
#include<stdlib.h>

struct PointerLimit{
    struct PointerLimit* next;
    int value;
} typedef PointerLimit;

PointerLimit* getTail(PointerLimit* base){
    while(base->next != NULL){
        base = base->next;
    }
    return base;
}

PointerLimit* add(PointerLimit* base, int value){
    PointerLimit* new = (PointerLimit*)malloc(sizeof(PointerLimit));
    new->value = value;
    new->next = NULL;
    PointerLimit* tail = getTail(base);
    tail->next = new;
    return base;
}

void test(PointerLimit* base){
    // Using "hard code" here. To check the limit of arrow operator.
    int temp = base->next->next->next->next->next->next->value;

    printf("Testing the limit of arrow operator. temp is %d.\n", temp);
}

int main(int argc, char** argv){
    if (argc <= 1){
        puts("require argment.");
        exit(1);
    }

    srand((unsigned int)0);

    PointerLimit* base = (PointerLimit*)malloc(sizeof(PointerLimit));
    base->next = NULL;
    base->value = -1;

    for (int i = 0; i < atoi(argv[1]); i++){
        add(base, rand()%100);
    }

    test(base);
}

これで、引数に与えた数だけ連結リストを作ってくれます。test()だけ、検証のコンセプト的にハードコードが必要なんですが、数千個コピペして繋げるのは大変です。ここを書いてくれるコードも書きました。

#include<stdio.h>
#include<stdlib.h>

int main(int argc, char **argv){
    if (argc <= 1){
        puts("require argment.");
        exit(1);
    }
    printf("int temp = base->");
    for (int i = 0; i < atoi(argv[1]); i++){
        printf("next->");
    }
    puts("value;");
}

引数に与えた数のアローを繋いでくれるコードです。macならpbcopyとかで出力もらって、上のコードに貼り付けて検証していきます。

検証

128, 256, 512, ... とボーダーっぽいところを試してみましたが、意外と上限はなさそうです。で、一気に10000繋ぐと何やら怒られました。

yamana.yuta@HL01538 % gcc limit.c                
clang: error: unable to execute command: Illegal instruction: 4
clang: error: clang frontend command failed due to signal (use -v to see invocation)
Apple clang version 15.0.0 (clang-1500.3.9.4)
Target: arm64-apple-darwin23.5.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin
clang: note: diagnostic msg: 
********************

PLEASE ATTACH THE FOLLOWING FILES TO THE BUG REPORT:
Preprocessed source(s) and associated run script(s) are located at:
clang: note: diagnostic msg: /var/folders/5r/92j2jsw53hxf28q43hs_kk700000gn/T/limit-406942.c
clang: note: diagnostic msg: /var/folders/5r/92j2jsw53hxf28q43hs_kk700000gn/T/limit-406942.sh
clang: note: diagnostic msg: Crash backtrace is located in
clang: note: diagnostic msg: /Users/yamana.yuta/Library/Logs/DiagnosticReports/clang_<YYYY-MM-DD-HHMMSS>_<hostname>.crash
clang: note: diagnostic msg: (choose the .crash file that corresponds to your crash)
clang: note: diagnostic msg: 

********************

どうやら予期されていないクラッシュを踏んでるらしい。

何が起きてるかはとりあえず後回しにして、ボーダーを探します。二分探索で探しました。

5030...OK
5031...NG

でした。多分ここがボーダーだと思います。(コンパイルで落ちてるので乱数の値とかは関係ないはず)

何が起きたか

何もわからんのでとりあえずアセンブラ出してみます。

gcc limit.c -S

面白い発見ですが、アセンブラは生成できました。さっきのエラーでggったときも、アーキテクチャレベルの問題的なことが書いてあったので多分そういうことなんだと思います。

成功する版と落ちる版をdiff取りました。まあ当然のことですが、差分は1行だけですね。その1行も、

ldr	x8, [x8]

なので、数が多いとどうにかなりそうな感じはしない。。。

まとめっぽいもの

理由はわからないにしても表題の結論は出ました。有限で、今回の環境なら5030個まで、が答えです。

確実にわかったことは、

  1. 実行のたびに若干変わるとかはなくて、明確に5030がボーダー
  2. アーキテクチャレベルで解釈が失敗してる

多分こうだろうということは、

  1. 環境が違うと結果がまるっきり違いそう
  2. そもそもこんなコード書く方が悪そう

でした。現場からは以上です。機会があれば環境変えてやってみようと思います。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.